 /*******************************************************************************
  * Copyright (c) 2007 IBM Corporation and others.
  * All rights reserved. This program and the accompanying materials
  * are made available under the terms of the Eclipse Public License v1.0
  * which accompanies this distribution, and is available at
  * http://www.eclipse.org/legal/epl-v10.html
  *
  * Contributors:
  * IBM Corporation - initial API and implementation
  *******************************************************************************/
 package org.eclipse.core.internal.net;

 import java.net.MalformedURLException ;
 import java.net.URL ;
 import java.util.*;

 import org.eclipse.core.net.proxy.IProxyData;
 import org.eclipse.core.runtime.*;
 import org.eclipse.core.runtime.preferences.IEclipsePreferences;
 import org.eclipse.core.runtime.preferences.IEclipsePreferences.*;
 import org.eclipse.osgi.util.NLS;
 import org.osgi.service.prefs.BackingStoreException;
 import org.osgi.service.prefs.Preferences;

 public class ProxyType implements INodeChangeListener, IPreferenceChangeListener {

     /**
      * Preference keys
      */
     private static final String PREF_PROXY_DATA_NODE = "proxyData"; //$NON-NLS-1$
 private static final String PREF_PROXY_HOST = "host"; //$NON-NLS-1$
 private static final String PREF_PROXY_PORT = "port"; //$NON-NLS-1$
 private static final String PREF_PROXY_HAS_AUTH = "hasAuth"; //$NON-NLS-1$

     /**
      * Verification tags used when creating a proxy data
      */
     public static int DO_NOT_VERIFY = 1;
     public static int VERIFY_EMPTY = 2;
     public static int VERIFY_EQUAL = 4;
     
     /**
      * Constants that control the setting of the SOCKS system properties
      */
     private static final String PROP_SOCKS_SYSTEM_PROPERTY_HANDLING = "org.eclipse.net.core.setSocksSystemProperties"; //$NON-NLS-1$
 public static final int ONLY_SET_FOR_1_5_OR_LATER = 0;
     public static final int ALWAYS_SET = 1;
     public static final int NEVER_SET = 2;
     public static int socksSystemPropertySetting;
     
     /*
      * Fields used to cache authentication information in the keyring
      */
     private static final String INFO_PROXY_USER = "user"; //$NON-NLS-1$
 private static final String INFO_PROXY_PASS = "pass"; //$NON-NLS-1$
 private static final URL FAKE_URL;
     static {
         URL temp = null;
         try {
             temp = new URL ("http://org.eclipse.core.net.proxy.auth");//$NON-NLS-1$
 } catch (MalformedURLException e) {
             // Should never fail
 }
         FAKE_URL = temp;
         String value = System.getProperty(PROP_SOCKS_SYSTEM_PROPERTY_HANDLING);
         if (value == null) {
             socksSystemPropertySetting = ONLY_SET_FOR_1_5_OR_LATER;
         } else if (value.equals("always")) { //$NON-NLS-1$
 socksSystemPropertySetting = ALWAYS_SET;
         } else if (value.equals("never")) { //$NON-NLS-1$
 socksSystemPropertySetting = NEVER_SET;
         } else {
             socksSystemPropertySetting = ONLY_SET_FOR_1_5_OR_LATER;
         }
     }
     
     private String name;
     private boolean updatingPreferences;

     public static String convertHostsToPropertyString(String [] value) {
         StringBuffer buffer = new StringBuffer ();

         if (value == null)
             return ""; //$NON-NLS-1$

         if (value.length > 0) {
             buffer.append(value[0]);
         }

         for (int index = 1; index < value.length; index++) {
             buffer.append('|');
             buffer.append(value[index]);
         }

         return buffer.toString();
     }

     public static String [] convertPropertyStringToHosts(String property) {
         return property.split("\\|"); //$NON-NLS-1$
 }

     public ProxyType(String name) {
         super();
         this.name = name;
     }

     private Preferences getPreferenceNode() {
         return getParentPreferences().node(getName());
     }

     /**
      * Return the preferences node whose child nodes are the know proxy types
      *
      * @return a preferences node
      */
     private Preferences getParentPreferences() {
         return Activator.getInstance().getInstancePreferences().node(
                 PREF_PROXY_DATA_NODE);
     }

     public IProxyData getProxyData(int verifyFlag) {
         return createProxyData(name, getPreferenceNode(), verifyFlag);
     }

     private IProxyData createProxyData(String type, Preferences node, int verifyFlag) {
         String host = node.get(PREF_PROXY_HOST, null);
         if (host != null && host.length() == 0)
             host = null;
         int port = node.getInt(PREF_PROXY_PORT, -1);
         boolean requiresAuth = node.getBoolean(PREF_PROXY_HAS_AUTH, false);
         ProxyData proxyData = new ProxyData(type, host, port, requiresAuth);
         loadProxyAuth(proxyData);
         if (verifyFlag == VERIFY_EMPTY) {
             // We are initializing so verify that the system properties are empty
 verifySystemPropertiesEmpty(type);
         } else if (verifyFlag == VERIFY_EQUAL) {
             // Verify that the data in the preferences matches the system properties
 verifyDataMatchesSystemProperties(proxyData);
         }
         return proxyData;
     }

     public boolean setProxyData(IProxyData proxyData, boolean proxiesEnabled) {
         Assert.isTrue(proxyData.getType().equals(getName()));
         IProxyData oldData = getProxyData(VERIFY_EQUAL);
         if (oldData.equals(proxyData))
             return false;
         saveProxyAuth(proxyData);
         try {
             updatingPreferences = true;
             updatePreferences(proxyData);
         } finally {
             updatingPreferences = false;
         }
         updateSystemProperties(proxyData, proxiesEnabled);
         return true;
     }

     private void updatePreferences(IProxyData proxyData) {
         updatePreferences(getPreferenceNode(), proxyData);
     }
     
     /* package */ void updatePreferencesIfMissing(Preferences node, IProxyData proxyData) {
         Preferences proxyNode = node.node(PREF_PROXY_DATA_NODE).node(getName());
         if (node.get(PREF_PROXY_HOST, null) == null)
             updatePreferences(proxyNode, proxyData);
     }
     
     private void updatePreferences(Preferences node, IProxyData proxyData) {
         if (proxyData.getHost() == null) {
             try {
                 Preferences parent = node.parent();
                 node.removeNode();
                 parent.flush();
             } catch (BackingStoreException e) {
                 Activator.logError(NLS.bind(
                         "An error occurred removing the {0} proxy node from the preference store", proxyData.getType()), e); //$NON-NLS-1$
 }
         } else {
             node.put(PREF_PROXY_HOST, proxyData.getHost());
             node.putInt(PREF_PROXY_PORT, proxyData.getPort());
             node.putBoolean(PREF_PROXY_HAS_AUTH, proxyData.getUserId() != null);
             try {
                 node.flush();
             } catch (BackingStoreException e) {
                 Activator.logError(NLS.bind(
                     "The {0} proxy node could not be written", proxyData.getType()), e); //$NON-NLS-1$
 }
         }
     }
     
     /* package */void updateSystemProperties(IProxyData proxyData, boolean proxiesEnabled) {
         try {
             if (proxyData.getType().equals(IProxyData.HTTP_PROXY_TYPE)) {
                 updateHttpSystemProperties(proxyData, proxiesEnabled);
             } else if (proxyData.getType().equals(IProxyData.HTTPS_PROXY_TYPE)) {
                 updateHttpsSystemProperties(proxyData, proxiesEnabled);
             } else if (proxyData.getType().equals(IProxyData.SOCKS_PROXY_TYPE)) {
                 updateSocksSystemProperties(proxyData, proxiesEnabled);
             }
         } catch (SecurityException e) {
             Activator.logError("A security exception occurred while trying to put the proxy data into the system properties", e); //$NON-NLS-1$
 }
     }
     
     private boolean verifyDataMatchesSystemProperties(ProxyData proxyData) {
         try {
             boolean proxiesEnabled = ProxyManager.getProxyManager().isProxiesEnabled();
             if (proxyData.getType().equals(IProxyData.HTTP_PROXY_TYPE)) {
                 return verifyDataMatchesHttpSystemProperties(proxyData, proxiesEnabled);
             } else if (proxyData.getType().equals(IProxyData.HTTPS_PROXY_TYPE)) {
                 return verifyDataMatchesHttpsSystemProperties(proxyData, proxiesEnabled);
             } else if (proxyData.getType().equals(IProxyData.SOCKS_PROXY_TYPE)) {
                 return verifyDataMatchesSocksSystemProperties(proxyData, proxiesEnabled);
             }
             
         } catch (SecurityException e) {
             // Just ignore this here since it will be surfaced elsewhere
 }
         return true;
     }

     private boolean verifyDataMatchesHttpSystemProperties(ProxyData proxyData,
             boolean proxiesEnabled) {
         if (proxiesEnabled) {
             boolean verified = true;
             String dHost = proxyData.getHost();
             if (!verifySystemPropertyEquals("http.proxyHost", dHost)) { //$NON-NLS-1$
 verified = false;
             } else if (dHost != null && !Boolean.getBoolean("http.proxySet")) { //$NON-NLS-1$
 Activator.logInfo("The HTTP proxy is enabled in the preferences but disabled in the system settings", null); //$NON-NLS-1$
 verified = false;
             }
             int port = proxyData.getPort();
             if (!verifySystemPropertyEquals("http.proxyPort", port == -1 ? null : String.valueOf(port))) { //$NON-NLS-1$
 verified = false;
             }
             return verified;
         }
         return verifyHttpSystemPropertiesEmpty();
     }

     private boolean verifyDataMatchesHttpsSystemProperties(ProxyData proxyData,
             boolean proxiesEnabled) {
         if (proxiesEnabled) {
             boolean verified = true;
             String dHost = proxyData.getHost();
             if (!verifySystemPropertyEquals("https.proxyHost", dHost)) { //$NON-NLS-1$
 verified = false;
             } else if (dHost != null && !Boolean.getBoolean("https.proxySet")) { //$NON-NLS-1$
 Activator.logInfo("The SSL proxy is enabled in the preferences but disabled in the system settings", null); //$NON-NLS-1$
 verified = false;
             }
             int port = proxyData.getPort();
             if (!verifySystemPropertyEquals("https.proxyPort", port == -1 ? null : String.valueOf(port))) { //$NON-NLS-1$
 verified = false;
             }
             return verified;
         }
         return verifyHttpsSystemPropertiesEmpty();
     }

     private boolean verifyDataMatchesSocksSystemProperties(ProxyData proxyData,
             boolean proxiesEnabled) {
         if (proxiesEnabled && shouldSetSocksSystemProperties()) {
             boolean verified = true;
             String dHost = proxyData.getHost();
             if (!verifySystemPropertyEquals("socksProxyHost", dHost)) { //$NON-NLS-1$
 verified = false;
             }
             int port = proxyData.getPort();
             if (!verifySystemPropertyEquals("socksProxyPort", port == -1 ? null : String.valueOf(port))) { //$NON-NLS-1$
 verified = false;
             }
             return verified;
         }
         return verifySocksSystemPropertiesEmpty();
     }
     
     private boolean shouldSetSocksSystemProperties() {
         if (socksSystemPropertySetting == ALWAYS_SET)
             return true;
         if (socksSystemPropertySetting == NEVER_SET)
             return false;
         return hasJavaNetProxyClass();
     }

     private boolean verifySystemPropertyEquals(String key, String expected) {
         String value = System.getProperty(key);
         if (value == expected)
             return true;
         if (value == null && expected != null) {
             Activator.logInfo(NLS.bind("System property {0} is not set but should be {1}.", key, expected), null); //$NON-NLS-1$
 return false;
         }
         if (value != null && expected == null) {
             Activator.logInfo(NLS.bind("System property {0} is set to {1} but should not be set.", key, value), null); //$NON-NLS-1$
 return false;
         }
         if (!value.equals(expected)) {
             Activator.logInfo(NLS.bind("System property {0} is set to {1} but should be {2}.", new Object [] {key, value, expected }), null); //$NON-NLS-1$
 return false;
         }
         return true;
     }

     private boolean verifySystemPropertiesEmpty(String proxyType) {
         try {
             if (proxyType.equals(IProxyData.HTTP_PROXY_TYPE)) {
                 return verifyHttpSystemPropertiesEmpty();
             } else if (proxyType.equals(IProxyData.HTTPS_PROXY_TYPE)) {
                 return verifyHttpsSystemPropertiesEmpty();
             } else if (proxyType.equals(IProxyData.SOCKS_PROXY_TYPE)) {
                 return verifySocksSystemPropertiesEmpty();
             }
         } catch (SecurityException e) {
             // Just ignore this here since it will be surfaced elsewhere
 }
         return true;
     }

     private boolean verifyHttpSystemPropertiesEmpty() {
         boolean verified = true;
         verified &= verifyIsNotSet("http.proxySet"); //$NON-NLS-1$
 verified &= verifyIsNotSet("http.proxyHost"); //$NON-NLS-1$
 verified &= verifyIsNotSet("http.proxyPort"); //$NON-NLS-1$
 verified &= verifyIsNotSet("http.nonProxyHosts"); //$NON-NLS-1$
 verified &= verifyIsNotSet("http.proxyUser"); //$NON-NLS-1$
 verified &= verifyIsNotSet("http.proxyUserName"); //$NON-NLS-1$
 verified &= verifyIsNotSet("http.proxyPassword"); //$NON-NLS-1$
 return verified;
     }

     private boolean verifyIsNotSet(String key) {
         String value = System.getProperty(key);
         if (value != null) {
             Activator.logInfo(NLS.bind("System property {0} has been set to {1} by an external source. This value will be overwritten using the values from the preferences", key, value), null); //$NON-NLS-1$
 }
         return value == null;
     }

     private boolean verifyHttpsSystemPropertiesEmpty() {
         boolean verified = true;
         verified &= verifyIsNotSet("https.proxySet"); //$NON-NLS-1$
 verified &= verifyIsNotSet("https.proxyHost"); //$NON-NLS-1$
 verified &= verifyIsNotSet("https.proxyPort"); //$NON-NLS-1$
 verified &= verifyIsNotSet("https.nonProxyHosts"); //$NON-NLS-1$
 verified &= verifyIsNotSet("https.proxyUser"); //$NON-NLS-1$
 verified &= verifyIsNotSet("https.proxyUserName"); //$NON-NLS-1$
 verified &= verifyIsNotSet("https.proxyPassword"); //$NON-NLS-1$
 return verified;
     }

     private boolean verifySocksSystemPropertiesEmpty() {
         boolean verified = true;
         verified &= verifyIsNotSet("socksProxyHost"); //$NON-NLS-1$
 verified &= verifyIsNotSet("socksProxyPort"); //$NON-NLS-1$
 return verified;
     }

     public String getName() {
         return name;
     }

     private void updateHttpSystemProperties(IProxyData data, boolean proxiesEnabled) {
         Assert.isTrue(data.getType().equals(IProxyData.HTTP_PROXY_TYPE));
         Properties sysProps = System.getProperties();
         if (!proxiesEnabled || data.getHost() == null) {
             sysProps.remove("http.proxySet"); //$NON-NLS-1$
 sysProps.remove("http.proxyHost"); //$NON-NLS-1$
 sysProps.remove("http.proxyPort"); //$NON-NLS-1$
 sysProps.remove("http.nonProxyHosts"); //$NON-NLS-1$
 sysProps.remove("http.proxyUser"); //$NON-NLS-1$
 sysProps.remove("http.proxyUserName"); //$NON-NLS-1$
 sysProps.remove("http.proxyPassword"); //$NON-NLS-1$
 } else {
             sysProps.put("http.proxySet", "true"); //$NON-NLS-1$ //$NON-NLS-2$
 sysProps.put("http.proxyHost", data.getHost()); //$NON-NLS-1$
 int port = data.getPort();
             if (port == -1) {
                 sysProps.remove("http.proxyPort"); //$NON-NLS-1$
 } else {
                 sysProps.put("http.proxyPort", String.valueOf(port)); //$NON-NLS-1$
 }
             sysProps.put("http.nonProxyHosts", //$NON-NLS-1$
 convertHostsToPropertyString(ProxyManager.getProxyManager().getNonProxiedHosts()));

             String userid = data.getUserId();
             String password = data.getPassword();
             if (userid == null || password == null || userid.length() == 0
                     || password.length() == 0) {
                 sysProps.remove("http.proxyUser"); //$NON-NLS-1$
 sysProps.remove("http.proxyUserName"); //$NON-NLS-1$
 sysProps.remove("http.proxyPassword"); //$NON-NLS-1$
 } else {
                 sysProps.put("http.proxyUser", userid); //$NON-NLS-1$
 sysProps.put("http.proxyUserName", userid); //$NON-NLS-1$
 sysProps.put("http.proxyPassword", password); //$NON-NLS-1$
 }
         }
     }
     
     private void updateHttpsSystemProperties(IProxyData data, boolean proxiesEnabled) {
         Assert.isTrue(data.getType().equals(IProxyData.HTTPS_PROXY_TYPE));
         Properties sysProps = System.getProperties();
         if (!proxiesEnabled || data.getHost() == null) {
             sysProps.remove("https.proxySet"); //$NON-NLS-1$
 sysProps.remove("https.proxyHost"); //$NON-NLS-1$
 sysProps.remove("https.proxyPort"); //$NON-NLS-1$
 sysProps.remove("https.nonProxyHosts"); //$NON-NLS-1$
 sysProps.remove("https.proxyUser"); //$NON-NLS-1$
 sysProps.remove("https.proxyUserName"); //$NON-NLS-1$
 sysProps.remove("https.proxyPassword"); //$NON-NLS-1$
 } else {
             sysProps.put("https.proxySet", "true"); //$NON-NLS-1$ //$NON-NLS-2$
 sysProps.put("https.proxyHost", data.getHost()); //$NON-NLS-1$
 int port = data.getPort();
             if (port == -1) {
                 sysProps.remove("https.proxyPort"); //$NON-NLS-1$
 } else {
                 sysProps.put("https.proxyPort", String.valueOf(port)); //$NON-NLS-1$
 }
             sysProps.put("https.nonProxyHosts", //$NON-NLS-1$
 convertHostsToPropertyString(ProxyManager.getProxyManager().getNonProxiedHosts()));

             String userid = data.getUserId();
             String password = data.getPassword();
             if (userid == null || password == null || userid.length() == 0
                     || password.length() == 0) {
                 sysProps.remove("https.proxyUser"); //$NON-NLS-1$
 sysProps.remove("https.proxyUserName"); //$NON-NLS-1$
 sysProps.remove("https.proxyPassword"); //$NON-NLS-1$
 } else {
                 sysProps.put("https.proxyUser", userid); //$NON-NLS-1$
 sysProps.put("https.proxyUserName", userid); //$NON-NLS-1$
 sysProps.put("https.proxyPassword", password); //$NON-NLS-1$
 }
         }
     }
     
     private void updateSocksSystemProperties(IProxyData data, boolean proxiesEnabled) {
         Assert.isTrue(data.getType().equals(IProxyData.SOCKS_PROXY_TYPE));
         Properties sysProps = System.getProperties();
         if (!proxiesEnabled || data.getHost() == null) {
             sysProps.remove("socksProxyHost"); //$NON-NLS-1$
 sysProps.remove("socksProxyPort"); //$NON-NLS-1$
 } else {
             if (!shouldSetSocksSystemProperties()) {
                 // Log an error if we are not setting the property because we are using a pre-1.5 JRE
 if (socksSystemPropertySetting == ONLY_SET_FOR_1_5_OR_LATER)
                     Activator.logError("Setting the SOCKS system properties for a 1.4 VM can interfere with other proxy services (e.g. JSch). Please upgrade to a 1.5 JRE or later if you need to use Java's SOCKS proxy support.", null); //$NON-NLS-1$
 return;
             }
             sysProps.put("socksProxyHost", data.getHost()); //$NON-NLS-1$
 int port = data.getPort();
             if (port == -1) {
                 sysProps.remove("socksProxyPort"); //$NON-NLS-1$
 } else {
                 sysProps.put("socksProxyPort", String.valueOf(port)); //$NON-NLS-1$
 }
             // TODO: There does appear to be a way to set the non-proxy hosts for Socks
 // TODO: See http://java.sun.com/j2se/1.5.0/docs/guide/net/properties.html for a description
 // of how to set the Socks user and password
 }
     }

     public void initialize(boolean proxiesEnabled) {
         updateSystemProperties(getProxyData(VERIFY_EMPTY), proxiesEnabled);
         ((IEclipsePreferences)getParentPreferences()).addNodeChangeListener(this);
         ((IEclipsePreferences)getPreferenceNode()).addPreferenceChangeListener(this);
     }
     
     private Map getAuthInfo() {
         // Retrieve username and password from keyring.
 Map authInfo = Platform.getAuthorizationInfo(FAKE_URL, getName(), ""); //$NON-NLS-1$
 return authInfo != null ? authInfo : Collections.EMPTY_MAP;
     }

     private void loadProxyAuth(IProxyData data) {
         Map authInfo = getAuthInfo();
         data.setUserid((String )authInfo.get(INFO_PROXY_USER));
         data.setPassword((String )authInfo.get(INFO_PROXY_PASS));
     }
     
     private void saveProxyAuth(IProxyData data) {
         Map authInfo = getAuthInfo();
         if (authInfo.size() == 0) {
             authInfo = new java.util.HashMap (4);
         }
         String proxyUser = data.getUserId();
         if (proxyUser != null && data.getHost() != null) {
             authInfo.put(INFO_PROXY_USER, proxyUser);
         } else {
             authInfo.remove(INFO_PROXY_USER);
         }
         String proxyPass = data.getPassword();
         if (proxyPass != null && data.getHost() != null) {
             authInfo.put(INFO_PROXY_PASS, proxyPass);
         } else {
             authInfo.remove(INFO_PROXY_PASS);
         }
         try {
             if (authInfo.isEmpty()) {
                 Platform.flushAuthorizationInfo(FAKE_URL, getName(), ""); //$NON-NLS-1$
 } else {
                 Platform.addAuthorizationInfo(FAKE_URL, getName(), "", authInfo); //$NON-NLS-1$
 }
         } catch (CoreException e) {
             Activator.logError(e.getMessage(), e);
         }
     }
     
     private synchronized boolean hasJavaNetProxyClass() {
         try {
             Class proxyClass = Class.forName("java.net.Proxy"); //$NON-NLS-1$
 return proxyClass != null;
         } catch (ClassNotFoundException e) {
             // Ignore
 }
         return false;
     }

     public void added(NodeChangeEvent event) {
         // Add a preference listener so we'll get changes to the fields of the node
 if (event.getChild().name().equals(getName()))
             ((IEclipsePreferences)event.getChild()).addPreferenceChangeListener(this);
     }

     public void removed(NodeChangeEvent event) {
         // Nothing to do
 }

     public void preferenceChange(PreferenceChangeEvent event) {
         if (updatingPreferences)
             return;
         updateSystemProperties(getProxyData(DO_NOT_VERIFY), ProxyManager.getProxyManager().isProxiesEnabled());
         
     }

 }

